package com.jgoodies.validation;

import com.jgoodies.validation.view.ValidationComponentUtils;
import com.jgoodies.validation.view.ValidationResultViewFactory;

import javax.swing.*;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
import java.util.Vector;

/**
 * Created by babitsky.viacheslav
 * Date: 04.02.2008
 * Time: 21:58:38
 * Can display validation feedback icons "over" a content panel.
 * It observes a ValidationResultModel and creates icon labels
 * in a feedback layer of a {@link javax.swing.JLayeredPane} on top of the content layer.
 * To position the feedback labels, the content pane is traversed
 * and searched for text components that match a validation message key
 * in this panel's observed ValidationResultModel.<p>
 *
 * <strong>Note:</strong> This panel doesn't reserve space for the portion
 * used to display the overlaid feedback components. It has been designed
 * to not change the layout of the wrapped content. Therefore you must reserve
 * this space, or in other words, you must ensure that the wrapped content
 * provides enough space to display the overlaid components.
 * Since the current implementation positions the overlay components
 * in the lower left, just make sure that there are about 6 pixel to the left
 * and bottom of the input components that can be marked.<p>
 *
 * This panel handles two event types: <ol>
 * <li>the ValidationResultModel changes; in this case the set of visible
 * feedback components shall mark the input components that match the
 * new validation result. This is done by this class' internal
 * <code>ValidationResultChangeHandler</code> which in turn invokes
 * <code>#updateFeedbackComponents</code>.
 * <li>the content layout changes; the feedback components must then be
 * repositioned to reflect the position of the overlaid input components.
 * This is done by overriding <code>#validateTree</code> and invoking
 * <code>#repositionFeedBackComponents</code> after the child tree has
 * been laid out. The current simple but expensive implementation
 * updates all components.
 * </ol><p>
 *
 * TODO: Check how the wrapping mechanism shall work with
 * JSplitPanes, JTabbedPanes and CardPanels. At least provide
 * guidelines, how to wrap these panel types, or how to handle
 * these cases.<p>
 *
 * TODO: Turn this class into an abstract superclass.
 * Subclasses shall implement the feedback component creation
 * and specify where to locate the feedback component relative
 * to the underlying content component.<p>
 *
 * TODO: Consider adding a mechanism, so that components can be added
 * and removed later.
 *
 * @author Karsten Lentzsch
 * @version $Revision: 1.23 $
 */
public final class IconFeedbackPanel extends JLayeredPane {

    private static final int CONTENT_LAYER  = 1;
    private static final int FEEDBACK_LAYER = 2;

    /**
     * Holds the ValidationResult and reports changes in that result.
     * Used to update the state of the feedback components.
     */
    private final ValidationResultModel model;

    /**
     * Refers to the content panel that holds the content components.
     */
    private final JComponent content;
    private boolean updateBackground;
    private Hashtable<Object,Vector<Validatable>> validatableHashtable;

    // Instance Creation ******************************************************

    /**
     * Creates an IconFeedbackPanel on the given ValidationResultModel
     * using the specified content panel.<p>
     *
     * <strong>Note:</strong> Typically you should wrap component trees with
     * {@link #getWrappedComponentTree(ValidationResultModel, JComponent, boolean)},
     * not this constructor.<p>
     *
     * <strong>Note:</strong> You must not add or remove components
     * from the content once this constructor has been invoked.
     *
     * @param model      the ValidationResultModel to observe
     * @param content    the panel that contains the content components
     *
     * @throws NullPointerException if model or content is {@code null}.
     */
    public IconFeedbackPanel(ValidationResultModel model, JComponent content, boolean updateBackground) {
        this.updateBackground = updateBackground;
        if (model == null)
            throw new NullPointerException("The validation result model must not be null.");
        if (content == null)
            throw new NullPointerException("The content must not be null.");

        this.model   = model;
        this.content = content;
        validatableHashtable = new Hashtable<Object, Vector<Validatable>>();
        setLayout(new SimpleLayout());
        add(content, CONTENT_LAYER);
        initEventHandling();
    }

    public boolean isUpdateBackground() {
        return updateBackground;
    }

    public void setUpdateBackground(boolean updateBackground) {
        this.updateBackground = updateBackground;
    }

    // Convenience Code *******************************************************

    /**
     * Wraps the components in the given component tree with instances
     * of IconFeedbackPanel where necessary. Such a wrapper is required
     * for all JScrollPanes that contain multiple children and
     * for the root - unless it's a JScrollPane with multiple children.
     *
     * @param root    the root of the component tree to wrap
     * @return the wrapped component tree
     */
    public static JComponent getWrappedComponentTree(
            ValidationResultModel model,
            JComponent root, boolean updateBackground) {
        wrapComponentTree(model, root, updateBackground);
        return isScrollPaneWithUnmarkableView(root)
            ? root
            : new IconFeedbackPanel(model, root, updateBackground);
    }

    private static void wrapComponentTree(
            ValidationResultModel model,
            Container container, boolean updateBackground) {
        if (!(container instanceof JScrollPane)) {
            int componentCount = container.getComponentCount();
            for (int i = 0; i < componentCount; i++) {
                Component child = container.getComponent(i);
                if (child instanceof Container)
                    wrapComponentTree(model, (Container) child, updateBackground);
            }
            return;
        }
        JScrollPane scrollPane = (JScrollPane) container;
        JViewport viewport = scrollPane.getViewport();
        JComponent view = (JComponent) viewport.getView();
        if (isMarkable(view))
            return;
        // TODO: Consider adding the following sanity check:
        // the view must not be an IconFeedbackPanel
        Component wrappedView = new IconFeedbackPanel(model, view, updateBackground);
        viewport.setView(wrappedView);
        wrapComponentTree(model, view, updateBackground);
    }

    private static boolean isScrollPaneWithUnmarkableView(Component c) {
        if (!(c instanceof JScrollPane))
            return false;
        JScrollPane scrollPane = (JScrollPane) c;
        JViewport viewport = scrollPane.getViewport();
        JComponent view = (JComponent) viewport.getView();
        return !isMarkable(view);
    }


    // Initialization *********************************************************

    /**
     * Registers a listener with the validation result model that updates
     * the feedback components.
     */
    private void initEventHandling() {
        model.addPropertyChangeListener(
                ValidationResultModel.PROPERTYNAME_RESULT,
                new ValidationResultChangeHandler());
    }


    // Abstract Behavior ******************************************************

    /**
     * Creates and returns a validation feedback component
     * that shall overlay the specified content component.<p>
     *
     * This implementation returns a JLabel. The validation result's severity
     * is used to lookup the label's icon; the result's message text is set
     * as the label's tooltip text.<p>
     *
     * TODO: Turn this method into an abstract method if this class
     * becomes an abstract superclass of general feedback overlay panels.
     *
     * @param result            determines the label's icon and tooltip text
     * @param contentComponent  the component to get overlaid feedback
     * @return the feedback component that overlays the content component
     *
     * @throws NullPointerException if the result is {@code null}
     */
    private JLabel createFeedbackComponent(
            ValidationResult result,
            Component contentComponent) {
        Icon icon = ValidationResultViewFactory.getSmallIcon(result.getSeverity());
        JLabel label = new JLabel(icon);
        label.setToolTipText(getMessagesToolTipText(result));
        label.setSize(label.getPreferredSize());
        label.setLabelFor(contentComponent);
        return label;
    }


    /**
     * Returns a string representation of the given validation result,
     * intended for tool tips. Unlike {@link ValidationResult#getMessagesText()}
     * this method returns an HTML string. It is invoked by
     * {@link #createFeedbackComponent(ValidationResult, Component)}
     * and the result won't be empty.
     *
     * @param result provides the ValidationMessages to iterate over
     * @return an HTML representation of the given result
     *
     * @since 1.3.1
     */
    private static String getMessagesToolTipText(ValidationResult result) {
        StringBuilder builder = new StringBuilder("<html>");
        for (ValidationMessage message : result.getMessages()) {
            if (builder.length() > 6) {
                builder.append("<br>");
            }
            builder.append(message.formattedText());
        }
        builder.append("</html>");
        return builder.toString();
    }


    /**
     * Computes and returns the origin of the given feedback component
     * using the content component's origin.<p>
     *
     * This implementation returns a JLabel. The validation result's severity
     * is used to lookup the label's icon; the result's message text is
     * set as the label's tooltip text.<p>
     *
     * TODO: Turn this method into an abstract method if this class
     * becomes an abstract superclass of general feedback overlay panels.
     *
     * @param feedbackComponent  the component that overlays the content
     * @param contentComponent   the component to get overlaid feedback
     * @return the feedback component's origin
     *
     * @throws NullPointerException if the feedback component or content component
     *     is {@code null}
     */
    private Point getFeedbackComponentOrigin(
            JComponent feedbackComponent,
            Component contentComponent) {
        boolean isLTR = contentComponent.getComponentOrientation().isLeftToRight();
        int x = contentComponent.getX()
              + (isLTR ? 0 : contentComponent.getWidth() - 1)
              - feedbackComponent.getWidth() / 2;
        int y = contentComponent.getY()
              + contentComponent.getHeight()
              - feedbackComponent.getHeight() + 2;

        return new Point(x, y);
    }


    // Updating the Overlay Components ****************************************

    private void removeAllFeedbackComponents() {
        int componentCount = getComponentCount();
        for (int i = componentCount - 1; i >= 0; i--) {
            Component child = getComponent(i);
            int layer = getLayer(child);
            if (layer == FEEDBACK_LAYER)
                remove(i);
        }
    }

    // Updating the Overlay Components ****************************************

    private void removeAllFeedbackComponents(JComponent component) {
        int componentCount = getComponentCount();
        for (int i = componentCount - 1; i >= 0; i--) {
            Component child = getComponent(i);
            int layer = getLayer(child);
            if (layer == FEEDBACK_LAYER && child instanceof JLabel && ((JLabel)child).getLabelFor() == component){
                remove(i);
                break;
            }
        }
    }

    /**
     * Traverses the component tree starting at the given container
     * and creates a feedback component for each JTextComponent that
     * is associated with a message in the specified <code>keyMap</code>.<p>
     *
     * The arguments passed to the feedback component creation method
     * are the visited component and its associated validation sub result.
     * This sub result is requested from the specified <code>keyMap</code>
     * using the visited component's message key.
     *
     * @param container   the component tree root
     * @param keyMap      maps messages keys to associated validation results
     */
    private void visitComponentTree(Container container,
            Map<Object, ValidationResult> keyMap, int xOffset, int yOffset) {
        int componentCount = container.getComponentCount();
        for (int i = 0; i < componentCount; i++) {
            Component child = container.getComponent(i);
            if (!child.isVisible())
                continue;
            if (isMarkable(child)) {
                visitComponent(keyMap, xOffset, yOffset, child);
            } else if (isScrollPaneView(child)) {
                // Just do nothing.
            } else if (child instanceof Container) {
                visitComponentTree(
                        (Container) child,
                        keyMap,
                        xOffset + child.getX(),
                        yOffset + child.getY());
            }
        }
    }

    private void visitComponent(Map<Object, ValidationResult> keyMap, int xOffset, int yOffset, Component child) {
        if (isScrollPaneView(child)) {
            Component containerParent = child.getParent().getParent();
            addFeedbackComponent(
                    containerParent,
                    (JComponent) child,
                    keyMap,
                    xOffset - containerParent.getX(),
                    yOffset - containerParent.getY());
        } else {
            addFeedbackComponent(
                    child,
                    (JComponent) child,
                    keyMap,
                    xOffset,
                    yOffset);
        }
    }

    private static boolean isScrollPaneView(Component c) {
        Container container = c.getParent();
        Container containerParent = container.getParent();
        return (container       instanceof JViewport)
            && (containerParent instanceof JScrollPane);
    }


    /**
     * Checks and answers if the given component can be marked or not.<p>
     *
     * TODO: Check the combobox editable state.<p>
     *
     * TODO: Add the JSpinner to the list of markable components.
     *
     * @param component  the component to be checked
     * @return true if the given component can be marked, false if not
     */
    private static boolean isMarkable(Component component) {
//        return    component instanceof JTextComponent
//               || component instanceof JComboBox;
        return  component instanceof Validatable;
    }

    private void addFeedbackComponent(
            Component contentComponent,
            JComponent messageComponent,
            Map<Object, ValidationResult> keyMap, int xOffset, int yOffset) {
        ValidationResult result = getAssociatedResult(messageComponent, keyMap);
        JComponent feedbackComponent = createFeedbackComponent(result, contentComponent);
        if (feedbackComponent == null)
            return;
        add(feedbackComponent, Integer.valueOf(FEEDBACK_LAYER));
        Point overlayPosition = getFeedbackComponentOrigin(feedbackComponent, contentComponent);
        overlayPosition.translate(xOffset, yOffset);
        feedbackComponent.setLocation(overlayPosition);
    }


    /**
     * Returns the ValidationResult associated with the given component
     * using the specified validation result key map. Unlike
     * {@link com.jgoodies.validation.view.ValidationComponentUtils#getAssociatedResult(JComponent, Map)}
     * this method returns the empty result if the component has no keys set.
     *
     * @param comp     the component may be marked with a validation message key
     * @param keyMap   maps validation message keys to ValidationResults
     * @return the ValidationResult associated with the given component
     *     as provided by the specified validation key map
     *     or <code>ValidationResult.EMPTY</code> if the component has no message key set,
     *     or <code>ValidationResult.EMPTY</code> if no result is associated
     *     with the component
     */
    private static ValidationResult getAssociatedResult(
            JComponent comp, Map<Object, ValidationResult> keyMap) {
        ValidationResult result = ValidationComponentUtils.getAssociatedResult(comp, keyMap);
        return result == null
            ? ValidationResult.EMPTY
            : result;
    }


    // Event Handling *********************************************************

    private void updateFeedbackComponents() {
        removeAllFeedbackComponents();
        visitComponentTree(content, model.getResult().keyMap(), 0, 0);
        repaint();
    }

    /**
     * Ensures that the feedback components are repositioned.
     * Invoked by <code>#validate</code>, i. e. if this panel is laid out.<p>
     *
     * TODO: Improve this implementation to set only positions.
     * The current implementation removes all components and re-adds
     * them later.
     */
    private void repositionFeedbackComponents() {
        updateFeedbackComponents();
    }


    /**
     * Recursively descends the container tree and recomputes the
     * layout for any subtrees marked as needing it (those marked as
     * invalid). In addition to the superclass behavior, we reposition
     * the feedback components after the child components have been
     * validated.<p>
     *
     * We reposition the feedback components only, if this panel is visible;
     * if it becomes visible, #validateTree will be invoked.
     *
     * @see Container#validateTree()
     * @see #validate()
     * @see #invalidate()
     * @see #doLayout()
     * @see Component#setVisible(boolean)
     * @see LayoutManager
     */
    @Override
    protected void validateTree() {
        super.validateTree();
        if (isVisible()) {
            repositionFeedbackComponents();
        }
    }

    public void registerComponent(Validatable validatable) {
        Vector<Validatable> validatables = validatableHashtable.get(validatable.getSerialValue());
        if (validatables == null){
            validatables = new Vector<Validatable>();
            validatableHashtable.put(validatable.getSerialValue(), validatables);
        }
        validatables.add(validatable);
    }

    public void setOwnMessagesAndRevalidateOtherRegistredComponent(Object key,
                                                                   Vector<ValidationMessage> validationMessages,
                                                                   boolean justDoIt) {
        final ValidationResult validationResult = new ValidationResult();
        final Enumeration<Object> objectEnumeration = validatableHashtable.keys();
        while (objectEnumeration.hasMoreElements()) {
            Object keyTmp = objectEnumeration.nextElement();
            if (!keyTmp.equals(key)){
                final Vector<Validatable> validatables = validatableHashtable.get(keyTmp);
                for (int i = 0; i < validatables.size(); i++) {
                    Validatable validatable = validatables.elementAt(i);
                    final Vector<ValidationMessage> validationMessageVector = validatable.validateValue();
                    validationResult.addAll(validationMessageVector);
                }
            } else {
                validationResult.addAll(validationMessages);
            }
        }
        if (justDoIt && validationResult.equals(model.getResult())) {
            validateComponents();
        }
        model.setResult(validationResult);
    }

    public void setOwnMessages(Object serialValue, ValidationResult validationResult) {
        final Vector<Validatable> validatables = validatableHashtable.get(serialValue);
        for (int i = 0; i < validatables.size(); i++) {
            final JComponent jComponent = (JComponent) validatables.elementAt(i);
            removeAllFeedbackComponents(jComponent);
            int offsetX = 0;
            int offsetY = 0;
            Container container = jComponent.getParent();
            while (container != null && container != this){
                offsetX += container.getX();
                offsetY += container.getY();
                container = container.getParent();
            }
            visitComponent(validationResult.keyMap(), offsetX, offsetY, jComponent);

            if (updateBackground) {
                ValidationComponentUtils.updateComponentSeverityBackground(jComponent, validationResult);
            }
        }
        repaint();
    }

    public void validateValue() {
        setOwnMessagesAndRevalidateOtherRegistredComponent(-1, new Vector<ValidationMessage>(), true);
        ValidationComponentUtils.updateComponentTreeMandatoryBorder(this);
    }


    /**
     * Gets notified when the ValidationResult changed and updates
     * the feedback components.
     */
    private final class ValidationResultChangeHandler implements PropertyChangeListener {

        public void propertyChange(PropertyChangeEvent evt) {
            validateComponents();
        }

    }

    private void validateComponents() {
        updateFeedbackComponents();
        if (updateBackground) ValidationComponentUtils.updateComponentTreeSeverityBackground(content, model.getResult());
    }



    // Layout *****************************************************************

    /**
     * Used to lay out the content layer in the icon feedback JLayeredPane.
     * The content fills the parent's space; minimum and preferred size of
     * this layout are requested from the content panel.
     */
    private final class SimpleLayout implements LayoutManager {

        /**
         * If the layout manager uses a per-component string,
         * adds the component <code>comp</code> to the layout,
         * associating it
         * with the string specified by <code>name</code>.
         *
         * @param name the string to be associated with the component
         * @param comp the component to be added
         */
        public void addLayoutComponent(String name, Component comp) {
            // components are well known by the container
        }

        /**
         * Removes the specified component from the layout.
         * @param comp the component to be removed
         */
        public void removeLayoutComponent(Component comp) {
            // components are well known by the container
        }

        /**
         * Calculates the preferred size dimensions for the specified
         * container, given the components it contains.
         *
         * @param parent the container to be laid out
         * @return the preferred size of the given container
         * @see #minimumLayoutSize(Container)
         */
        public Dimension preferredLayoutSize(Container parent) {
            return content.getPreferredSize();
        }

        /**
         * Calculates the minimum size dimensions for the specified
         * container, given the components it contains.
         *
         * @param parent the component to be laid out
         * @return the minimum size of the given container
         * @see #preferredLayoutSize(Container)
         */
        public Dimension minimumLayoutSize(Container parent) {
            return content.getMinimumSize();
        }

        /**
         * Lays out the specified container.
         *
         * @param parent the container to be laid out
         */
        public void layoutContainer(Container parent) {
            Dimension size = parent.getSize();
            content.setBounds(0, 0, size.width, size.height);
        }

    }


}
